如何优雅的扫描指定包下的所有class 您所在的位置:网站首页 idea classloader 如何优雅的扫描指定包下的所有class

如何优雅的扫描指定包下的所有class

#如何优雅的扫描指定包下的所有class| 来源: 网络整理| 查看: 265

如果你是框架代码编写者,或者要学习如何编写框架,那么 获取指定包下所有class对象 这个操作时必不可少的。下面我来讲解下过程。

比如我们要扫描com.hadluo包下的A.class和B.class :

思路:

递归找出环境变量下指定包(com.hadluo)下面的所有以.class结尾的文件(也有可能是jar)。截取文件: F:..../com/hadluo 之前的不要,然后去掉.class文件后缀,结果:com/hadluo/A , com/hadluo/B 。将上面结果变成com.hadluo.A 然后通过Class.forName加载。如果是第一步是jar的话在进行jar内部扫描处理。方法一:通过JDK原生类实现

我们先了解下ClassLoader下的 getResources() 方法:

// 找出当前环境下/name 的所有的资源磁盘路径(包括jar包和一般文件) public Enumeration getResources(String name) ;

getResources会找出sun.boot.class.path和java.class.path解析出的路径集合中所有名称为name的资源。

ClassLoader三个加载路径:

sun.boot.class.path所代表的启动类路径java.ext.dirs所代表的扩展类路径java.class.path所代表的应用类路径

对于目录和jar包底层处理方式是不同的,目录是通过sun.misc.URLClassPath的内部类FileLoader处理的,jar包是通过sun.misc.URLClassPath的内部类JarLoader处理的

举个例子:

public class Test { public static void main(String[] args) throws NoSuchMethodException, SecurityException, IOException { Enumeration dirs = Thread.currentThread().getContextClassLoader().getResources("com"); while(dirs.hasMoreElements()) { System.err.println(dirs.nextElement()); } } } // 打印结果 , file是协议,代表是磁盘文件。 如果是jar:/代表是jar包 file:/D:/dev/scala_pro/test/bin/com

我们项目有com包,于是把我们的com包的磁盘路径打了出来。接下来只需要递归遍历这个路径获取后缀是.class的文件,然后根据class的名称,路径利用Class.forName加载成Class对象就行了,整个代码如下:

public class Test { public static void main(String[] args) throws NoSuchMethodException, SecurityException, IOException, ClassNotFoundException { // 结果 class List> loadeClasses(List classes,String scan) throws ClassNotFoundException { List>(); for(File file : classes) { // 因为scan 就是/ , 所有把 file的 / 转成 \ 统一都是: / String fPath = file.getAbsolutePath().replaceAll("\\\\","/") ; // 把 包路径 前面的 盘符等 去掉 , 这里必须是lastIndexOf ,防止名称有重复的 String packageName = fPath.substring(fPath.lastIndexOf(scan)); // 去掉后缀.class ,并且把 / 替换成 . 这样就是 com.hadluo.A 格式了 , 就可以用Class.forName加载了 packageName = packageName.replace(".class","").replaceAll("/", "."); // 根据名称加载类 clazzes.add(Class.forName(packageName)); } return clazzes ; } /** * 查找所有的文件 * * @param dir 路径 * @param fileList 文件集合 */ private static void listFiles(File dir, List fileList) { if (dir.isDirectory()) { for (File f : dir.listFiles()) { listFiles(f, fileList); } } else { if(dir.getName().endsWith(".class")) { fileList.add(dir); } } } } ////--------------------运行结果 com.hadluo.A com.hadluo.B

如果是jar 我们利用JarURLConnection 类来变量jar里面文件 , 我们接上面的url 来实验下jar的加载:

JarURLConnection urlConnection = (JarURLConnection) url.openConnection(); // 从此jar包 得到一个枚举类 Enumeration entries = urlConnection.getJarFile().entries(); // 遍历jar while (entries.hasMoreElements()) { // 获取jar里的一个实体 可以是目录 和一些jar包里的其他文件 如META-INF等文件 JarEntry entry = entries.nextElement(); //得到该jar文件下面的类实体 System.err.println(entry.getName()); }

我们先导入一个jedis.jar进来

然后 将扫描路径改成 redis , 运行上面程序:

redis/ redis/clients/ redis/clients/util/ redis/clients/jedis/ redis/clients/jedis/params/ redis/clients/util/MurmurHash.class redis/clients/util/JedisByteHashMap.class redis/clients/util/IOUtils.class redis/clients/util/RedisInputStream.class ..... ........ redis/clients/jedis/JedisCluster$1.class redis/clients/jedis/JedisCluster$10.class redis/clients/jedis/BinaryJedisCluster$37.class META-INF/maven/ META-INF/maven/redis.clients/ META-INF/maven/redis.clients/jedis/ META-INF/maven/redis.clients/jedis/pom.xml META-INF/maven/redis.clients/jedis/pom.properties

我们发现 把我们jar里面的所有class(包括内部类)都扫描出来了。而且把META-INF的资源文件也扫描出来了。

接下来我们只需要处理 .class结尾的文件就行了:

//file:/D:/dev/scala_pro/test/src/jedis-2.8.0.jar!/redis JarURLConnection urlConnection = (JarURLConnection) url.openConnection(); // 从此jar包 得到一个枚举类 Enumeration entries = urlConnection.getJarFile().entries(); // 遍历jar while (entries.hasMoreElements()) { // 获取jar里的一个实体 可以是目录 和一些jar包里的其他文件 如META-INF等文件 JarEntry entry = entries.nextElement(); //得到该jar文件下面的类实体 if(entry.getName().endsWith(".class")) { // 因为scan 就是/ , 所有把 file的 / 转成 \ 统一都是: / String fPath = entry.getName().replaceAll("\\\\","/") ; // 把 包路径 前面的 盘符等 去掉 String packageName = fPath.substring(fPath.lastIndexOf(scan)); // 去掉后缀.class ,并且把 / 替换成 . 这样就是 com.hadluo.A 格式了 , 就可以用Class.forName加载了 packageName = packageName.replace(".class","").replaceAll("/", "."); // 根据名称加载类 // System.err.println(packageName); // System.err.println(Class.forName(packageName).getName()); } }

整个 工具类代码 会在文章结尾给大家。

方法二:通过Spring工具类实现

这种方法就相当简单了,因为是站在巨人肩膀上,请让我来跟您分析分析。

先引入spring 包:

org.springframework spring-webmvc 5.1.6.RELEASE

然后在看实例:

import org.springframework.core.io.Resource; import org.springframework.core.io.support.PathMatchingResourcePatternResolver; public class Main { public static void main(String[] args) throws IOException { PathMatchingResourcePatternResolver resolver = new PathMatchingResourcePatternResolver(); // 加載資源 classpath*:com/hadluo/**/*.class : 找环境变量下的 com/hadluo下的 所有.class文件 Resource[] resources = resolver.getResources("classpath*:redis/**/*.class"); for(Resource res :resources) { System.err.println(res.getURI()); } } }

这样就把 redis.* 包下的所有class文件找出来了,包括jar包。

接下来 我们只需要获取 class的包路径,然后 Class.forName() 就可以了:

public class Main { public static void main(String[] args) throws IOException, ClassNotFoundException { PathMatchingResourcePatternResolver resolver = new PathMatchingResourcePatternResolver(); // 加載資源 classpath*:com/hadluo/**/*.class : 找环境变量下的 com/hadluo下的 所有.class文件 Resource[] resources = resolver.getResources("classpath*:com/luo/**/*.class"); for(Resource res :resources) { // 先获取resource的元信息,然后获取class元信息,最后得到 class 全路径 String clsName = new SimpleMetadataReaderFactory().getMetadataReader(res).getClassMetadata().getClassName(); // 通过名称加载 System.err.println(Class.forName(clsName).getName()); } } }

整个 过程相当之简单,比我们自己写!!!!

PathMatchingResourcePatternResolver 原理

其它的上层我就不追踪了,核心的加载就是下面这个方法:

protected Set doFindAllClassPathResources(String path) throws IOException { Set result = new LinkedHashSet(16); ClassLoader cl = getClassLoader(); // 通过classLoader的 getResources方法 Enumeration resourceUrls = (cl != null ? cl.getResources(path) : ClassLoader.getSystemResources(path)); while (resourceUrls.hasMoreElements()) { URL url = resourceUrls.nextElement(); result.add(convertClassLoaderURL(url)); } if ("".equals(path)) { // The above result is likely to be incomplete, i.e. only containing file system references. // We need to have pointers to each of the jar files on the classpath as well... addAllClassLoaderJarRoots(cl, result); } return result; }

发现 也是跟我们开头自己写的一样,通过 ClassLoader的 getResources 去加载 资源。万变不离其宗 ,框架代码也是在jdk基础上建立的。

提出一个问题:

如果你的项目是JDK7以下的话,需要谨慎传入包名,防止永久代溢出。比如说传入的是 com , 由于com可能会有很多不相关的类被加载,JDK7以下是需要占用永久代空间的。而且永久代是固定的,很容易由于 class 过多而OOM溢出。

线上永久代内存溢出 案例 请见我这篇文章:

最后奉献给大家这个工具类,如果没有依赖spring,也可以把 springScanf 方法删除,用jdk原生的。 最后希望大家会喜欢:

强烈推荐一个走向Java架构师道路的博客

本文完~



【本文地址】

公司简介

联系我们

今日新闻

    推荐新闻

    专题文章
      CopyRight 2018-2019 实验室设备网 版权所有